﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using msilgc.common;
using msilgc.common.Attributes;
using msilgc.runtime;
using msilgc.runtime.Platforms;

namespace Demo {
    public static class TestMethods {

        private static MethodInfo GetMethod(Type cls, string name) {
            foreach (MethodInfo method in cls.GetMethods()) {
                if (method.Name.CompareTo(name) == 0) {
                    return method;
                }
            }
            throw new Exception("Cannot find method: '" + name + "'");
        }

        public static void CountDivisors(
            int numberToTest, int stepSize, [OutParameter] int[] divisorCount, 
            [ThreadIdxParameter] Grain threadGrain, [BlockIdxParameter] Grain dataGrain) {

            int divisorToTest = dataGrain.X * stepSize + threadGrain.X;
            if(numberToTest % divisorToTest == 0) {
                divisorCount[threadGrain.X] = divisorCount[threadGrain.X] + 1;
            }
        }

        private static int Sum(IEnumerable<int> values)
        {
            int sum = 0;
            foreach (int value in values)
            {
                sum += value;
            }
            return sum;
        }

        public static int CountDivisors_CUDA()
        {
            const int testCount = 1000000000;
            const int threadCount = 200;
            
            IMethodHandle methodHandle =
                new Builder().CompileStaticMethod(GetMethod(typeof (TestMethods), "CountDivisors"), PlatformType.CUDA, false);
            methodHandle.Compile();
            //for(int i = testCount; i < testCount + threadCount*100; i += threadCount)
            int i = testCount;
            //{
                int stepSize = i / threadCount;
                Grain threadDim = new Grain(threadCount, 1, 1);
                Grain blockDim = new Grain(stepSize, 1, 1);
                int[] result = new int[threadCount];
                methodHandle.Call(threadDim, blockDim, testCount, stepSize, result);
                int summedResult = Sum(result);
                Console.WriteLine("RESULT: " + summedResult);
            //}
            return summedResult;
        }

        public static void RunMatrixMultiply_TestEmulator()
        {
            const int width = 2048;
            const int m = width, n = width;
            const int threadCount = 256;
            Grain threadDim = new Grain(threadCount, 1, 1);
            Grain blockDim = new Grain(m / threadCount, n, 1);
            IMethodHandle hostMethod =
                new Builder().CompileStaticMethod(
                GetMethod(typeof(SampleKernels.Sample1), "MatrixMultiplyDevice"), PlatformType.NATIVE, false);
            IMethodHandle cudaMethod =
                new Builder().CompileStaticMethod(
                GetMethod(typeof(SampleKernels.Sample1), "MatrixMultiplyDevice"), PlatformType.CUDA, false);
            hostMethod.Compile();
            cudaMethod.Compile();

            var a = new int[m * n];
            var b = new int[m * n];
            Populate(a, m * n); Populate(b, m * n);
            var emuC = new int[m*n];
            var devC = new int[m*n];

            DateTime deviceStart = DateTime.Now;
            cudaMethod.Call(threadDim, blockDim, a, b, devC, width, threadCount);
            TimeSpan gpuTime = DateTime.Now - deviceStart;
            Console.WriteLine(gpuTime.TotalSeconds);

            DateTime hostStart = DateTime.Now;
            hostMethod.Call(threadDim, blockDim, a, b, emuC, width, threadCount);
            TimeSpan hostTime = DateTime.Now - hostStart;
            Console.WriteLine(hostTime.TotalSeconds);

            Compare(devC, emuC);
        }

        private static int GetArraySize<T>(T[] array)
        {
            int bytes = Marshal.SizeOf(typeof (T))*array.Length;
            int megaBytes = bytes/1024/1024;
            return megaBytes;
        }

        public static void RunMatrixMultiply_CUDA()
        {
            bool debug = false;
            const int width = 6000;
            const int m = width, n = width;
            const int threadCount = 512;
            Grain threadDim = new Grain(threadCount, 1, 1);
            Grain blockDim = new Grain(m/threadCount, n, 1);
            IMethodHandle methodHandle =
                new Builder().CompileStaticMethod(
                GetMethod(typeof(SampleKernels.Sample1), "MatrixMultiplyDevice"), PlatformType.CUDA, debug);
            string err = "";
            try
            {
                methodHandle.Compile();
            }
            catch(Exception e) {
                err += string.Format("Exception - {0} - {1}", e.GetType(), e.Message) + Environment.NewLine;
                err += "Compiler Messages: " + Environment.NewLine;
                err += methodHandle.CompilerMessages + Environment.NewLine;
            }
            finally
            {
                if (debug && !string.IsNullOrEmpty(err))
                {
                    err += "Kernel Code: " + Environment.NewLine;
                    err += methodHandle.GeneratedKernelCode + Environment.NewLine;
                }
            }
            if(!string.IsNullOrEmpty(err))
            {
                throw new Exception(err);
            }
            var a = new int[m * n];
            var b = new int[m * n];
            Populate(a, m * n); Populate(b, m * n);

            Console.WriteLine("Memory used = " + (GetArraySize(a) * 3));
            Console.WriteLine("GPU Time, CPU Time (x16), CPU Time");

            var deviceC = new int[m * n];

            GCHandle ha = GCHandle.Alloc(a, GCHandleType.Pinned);
            GCHandle hb = GCHandle.Alloc(b, GCHandleType.Pinned);
            GCHandle hc = GCHandle.Alloc(deviceC, GCHandleType.Pinned);
            try
            {

                for (int i = 0; i < 2; i++)
                {
                    //var hostMTC = new int[m*n];
                    //var hostC = new int[m*n];

                    //DateTime deviceStart = DateTime.Now;
                    methodHandle.MeasurePerformance = true;
                    methodHandle.Call(threadDim, blockDim, a, b, deviceC, width, threadCount);
                    Console.WriteLine("{0} - {1}", i, methodHandle.ExecutionTime);
                    //TimeSpan gpuTime = DateTime.Now - deviceStart;
                    //Console.WriteLine(gpuTime.TotalSeconds);

                    /*DateTime hostMTStart = DateTime.Now;
                    SampleKernels.Sample1.MatrixMultiplyHostMT(a, b, hostMTC, width);
                    TimeSpan cpuMTTime = DateTime.Now - hostMTStart;
                    Console.Write(cpuMTTime.TotalSeconds + ", ");*/

                    /*DateTime hostStart = DateTime.Now;
                    SampleKernels.Sample1.MatrixMultiplyHost(a, b, hostC, width);
                    TimeSpan cpuTime = DateTime.Now - hostStart;
                    Console.WriteLine(cpuTime.TotalSeconds);

                    Compare(deviceC, hostC);*/
                    /*Compare(deviceC, hostMTC);
                    Compare(hostC, hostMTC);*/
                }
            }
            finally
            {
                ha.Free();
                hb.Free();
                hc.Free();
            }

            Console.WriteLine(methodHandle.GeneratedKernelCode);
        }

        private static void Compare(int[] a, int[] b)
        {
            for(int i = 0; i < a.Length; i++)
            {
                if(a[i] != b[i])
                {
                    Console.WriteLine("Difference at offset " + i);
                    return;
                }
            }
        }

        /*private static void Print(int[] array, int m, int n)
        {
            StringBuilder buffer = new StringBuilder(10000);
            for(int i = 0; i < m; i++)
            {
                for(int j = 0; j < n; j++)
                {
                    buffer.Append(array[i*m + j]);
                    buffer.Append(' ');
                }
                buffer.AppendLine();
            }
            Console.WriteLine(buffer.ToString());
        }*/

        private static void Populate(int[] array, int x)
        {
            for (int i = 0; i < array.Length; i++)
            {
                array[i] = i%255;
            }
        }

        public static IMethodHandle Compile_TestPrimeCUDA()
        {
            const string methodName = "TestPrimes_Parallel_EvenWorkload";
            MethodInfo method = GetMethod(typeof(SampleKernels.Sample1), methodName);
            IMethodHandle methodHandle = new Builder().CompileStaticMethod(method, PlatformType.CUDA, false);
            methodHandle.Compile(); //compile ahead of time
            return methodHandle;
        }

        public static void TestPrime_CUDA(IMethodHandle methodHandle, int lowCount, int testCount) {
            int[] gpuDivisorCount = new int[testCount];
            for (int i = 0; i < testCount; i++) {
                gpuDivisorCount[i] = 0;
            }

            const int threadCount = 512;

            int dataGrain = testCount / threadCount;
            int stepSize = testCount / (testCount / threadCount);

            Grain threadDim = new Grain(threadCount, 1, 1);
            Grain blockDim = new Grain(1, 1, 1);

            methodHandle.Call(threadDim, blockDim, gpuDivisorCount, lowCount, dataGrain, stepSize);
        }

        public static void TestPrime_Intel(int lowCount, int testCount) {
            int[] cpuDivisorCount = new int[testCount];
            for (int i = 0; i < testCount; i++) {
                cpuDivisorCount[i] = 0;
            }
            const int threadCount = 200;
            int dataGrain = testCount / threadCount;
            int stepSize = testCount / (testCount / threadCount);
            Grain threadGrain = new Grain(threadCount, 1, 1);
            for (int i = 0; i < threadCount; i++) {
                threadGrain.X = i;
                SampleKernels.Sample1.TestPrimes_Parallel_EvenWorkload(
                    cpuDivisorCount, lowCount, dataGrain, stepSize, threadGrain);
            }
        }
    }
}
